/**
 * jquery.cxassette.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * 
 * Copyright 2012, Codrops
 * http://www.codrops.com
 */

;
(function (window, $, undefined) {
	var aux = {
		getSupportedType: function () {
			var audio = document.createElement('audio');
			if (audio.canPlayType('audio/mpeg;')) {
				return 'mp3';
			} else if (audio.canPlayType('audio/ogg;')) {
				return 'ogg';
			} else {
				return 'wav';
			}
		}

	};

	// Cassette obj
	$.Cassette = function (options, element) {
		this.$el = $(element);
		this._init(options);
	};

	$.Cassette.defaults = {
		// song names. Assumes the path of each song is songs/name.filetype
		songs: ['beichuang', 'tuerqi', 'chenzao', 'Croatian_Rhapsody'],
		fallbackMessage: 'HTML5 audio not supported',
		// initial sound volume
		initialVolume: 0.7
	};

	$.Cassette.prototype = {
		_init: function (options) {
			var _self = this;
			// the options
			this.options = $.extend(true, {}, $.Cassette.defaults, options);
			// current side of the tape
			this.currentSide = 1;
			// current time of playing side
			this.cntTime = 0;
			// current sum of the duration of played songs 
			this.timeIterator = 0;
			// used for rewind / forward
			this.elapsed = 0.0;
			// action performed
			this.lastaction = '';
			// if play / forward / rewind active..
			this.isMoving = false;

			this.$loader = this.$el.find('div.vc-loader').show();

			// create cassette sides
			$.when(this._createSides()).done(function () {

				_self.$loader.hide();

				// create player
				_self._createPlayer();
				_self.sound = new $.Sound();
				// load events
				_self._loadEvents();

			});

		},
		_getSide: function () {
			return (this.currentSide === 1) ? {
				current: this.side1,
				reverse: this.side2
			} : {
				current: this.side2,
				reverse: this.side1
			};
		},
		// songs are distributed equally on both sides
		_createSides: function () {

			var playlistSide1 = [],
				playlistSide2 = [],
				_self = this,
				cnt = 0;

			return $.Deferred(
				function (dfd) {
					for (var i = 0, len = _self.options.songs.length; i < len; ++i) {
						var song = new $.Song(_self.options.songs[i], i);
						$.when(song.loadMetadata()).done(function (song) {
							(song.id < len / 2) ? playlistSide1.push(song): playlistSide2.push(song);
							++cnt;
							if (cnt === len) {
								// two sides, each side with multiple songs
								_self.side1 = new $.Side('side1', playlistSide1, 'start'),
									_self.side2 = new $.Side('side2', playlistSide2, 'end');

								dfd.resolve();
							}
						});
					}
				}
			).promise();
		},
		_createPlayer: function () {

			// create HTML5 audio element
			this.$audioEl = $('<audio id="audioElem"><span>' + this.options.fallbackMessage + '</span></audio>');
			this.$el.prepend(this.$audioEl);

			this.audio = this.$audioEl.get(0);

			// create custom controls
			this._createControls();

			this.$theTape = this.$el.find('div.vc-tape');
			this.$wheelLeft = this.$theTape.find('div.vc-tape-wheel-left');
			this.$wheelRight = this.$theTape.find('div.vc-tape-wheel-right');
			this.$tapeSideA = this.$theTape.find('div.vc-tape-side-a').show();
			this.$tapeSideB = this.$theTape.find('div.vc-tape-side-b');

		},
		_createControls: function () {

			var _self = this;

			this.$controls = $('<ul class="vc-controls" style="display:none;"/>');

			this.$cPlay = $('<li class="vc-control-play">Play<span></span></li>');
			this.$cRewind = $('<li class="vc-control-rewind">Rew<span></span></li>');
			this.$cForward = $('<li class="vc-control-fforward">FF<span></span></li>');
			this.$cStop = $('<li class="vc-control-stop">Stop<span></span></li>');
			this.$cSwitch = $('<li class="vc-control-switch">Switch<span></span></li>');

			this.$controls.append(this.$cPlay)
				.append(this.$cRewind)
				.append(this.$cForward)
				.append(this.$cStop)
				.append(this.$cSwitch)
				.appendTo(this.$el);

			this.$volume = $('<div style="display:none;" class="vc-volume-wrap"><div class="vc-volume-control"><div class="vc-volume-knob"></div></div></div> ').appendTo(this.$el);

			if (document.createElement('audio').canPlayType) {

				if (!document.createElement('audio').canPlayType('audio/mpeg') && !document.createElement('audio').canPlayType('audio/ogg')) {
					// TODO: flash fallback!
				} else {
					this.$controls.show();
					this.$volume.show();
					this.$volume.find('div.vc-volume-knob').knobKnob({
						snap: 10,
						value: 359 * this.options.initialVolume,
						turn: function (ratio) {

							_self._changeVolume(ratio);

						}
					});
					this.audio.volume = this.options.initialVolume;
				}

			}

		},
		_loadEvents: function () {

			var _self = this;

			this.$cSwitch.on('mousedown', function (event) {

				_self._setButtonActive($(this));
				_self._switchSides();

			});

			this.$cPlay.on('mousedown', function (event) {

				_self._setButtonActive($(this));
				_self._play();

			});

			this.$cStop.on('mousedown', function (event) {

				_self._setButtonActive($(this));
				_self._stop();

			});

			this.$cForward.on('mousedown', function (event) {

				_self._setButtonActive($(this));
				_self._forward();

			});

			this.$cRewind.on('mousedown', function (event) {

				_self._setButtonActive($(this));
				_self._rewind();

			});

			this.$audioEl.on('timeupdate', function (event) {

				_self.cntTime = _self.timeIterator + _self.audio.currentTime;
				var wheelVal = _self._getWheelValues(_self.cntTime);
				_self._updateWheelValue(wheelVal);

			});

			this.$audioEl.on('loadedmetadata', function (event) {

			});

			this.$audioEl.on('ended', function (event) {

				_self.timeIterator += _self.audio.duration;
				_self._play();

			});

		},
		_setButtonActive: function ($button) {

			// TODO. Solve! For now:
			$button.addClass('vc-control-pressed');

			setTimeout(function () {

				$button.removeClass('vc-control-pressed');

			}, 100);

		},
		_updateAction: function (action) {

			this.lastaction = action;

		},
		_prepare: function (song) {

			this._clear();
			this.$audioEl.attr('src', song.getSource(aux.getSupportedType()));

		},
		_switchSides: function () {

			if (this.isMoving) {
				return false;
			}

			this.sound.play('switch');

			var _self = this;

			this.lastaction = '';

			if (this.currentSide === 1) {

				this.currentSide = 2;

				this.$theTape.css({
					'-webkit-transform': 'rotate3d(0, 1, 0, 180deg)',
					'-moz-transform': 'rotate3d(0, 1, 0, 180deg)',
					'-o-transform': 'rotate3d(0, 1, 0, 180deg)',
					'-ms-transform': 'rotate3d(0, 1, 0, 180deg)',
					'transform': 'rotate3d(0, 1, 0, 180deg)'
				});

				setTimeout(function () {

					_self.$tapeSideA.hide();
					_self.$tapeSideB.show();

					// update wheels
					_self.cntTime = _self._getPosTime();

				}, 200);

			} else {

				this.currentSide = 1;

				this.$theTape.css({
					'-webkit-transform': 'rotate3d(0, 1, 0, 0deg)',
					'-moz-transform': 'rotate3d(0, 1, 0, 0deg)',
					'-o-transform': 'rotate3d(0, 1, 0, 0deg)',
					'-ms-transform': 'rotate3d(0, 1, 0, 0deg)',
					'transform': 'rotate3d(0, 1, 0, 0deg)'
				});

				setTimeout(function () {

					_self.$tapeSideB.hide();
					_self.$tapeSideA.show();

					// update wheels
					_self.cntTime = _self._getPosTime();

				}, 200);

			}

		},
		_updateButtons: function (button) {

			var pressedClass = 'vc-control-active';

			this.$cPlay.removeClass(pressedClass);
			this.$cStop.removeClass(pressedClass);
			this.$cRewind.removeClass(pressedClass);
			this.$cForward.removeClass(pressedClass);

			switch (button) {

				case 'play':
					this.$cPlay.addClass(pressedClass);
					break;
				case 'rewind':
					this.$cRewind.addClass(pressedClass);
					break;
				case 'forward':
					this.$cForward.addClass(pressedClass);
					break;

			}

		},
		_changeVolume: function (ratio) {
			this.audio.volume = ratio;
		},
		_play: function () {
			var _self = this;
			this._updateButtons('play');
			$.when(this.sound.play('click')).done(function () {
				var data = _self._updateStatus();
				if (data) {
					_self._prepare(_self._getSide().current.getSong(data.songIdx));
					_self.$audioEl.on('canplay', function (event) {
						$(this).off('canplay');
						_self.audio.currentTime = data.timeInSong;
						_self.audio.play();
						_self.isMoving = true;

						_self._setWheelAnimation('2s', 'play');

					});

				}

			});

		},
		_updateStatus: function (buttons) {

			var posTime = this.cntTime;

			// first stop
			this._stop(true);

			this._setSidesPosStatus('middle');

			// the current time to play is this.cntTime +/- [this.elapsed]
			if (this.lastaction === 'forward') {

				posTime += this.elapsed;

			} else if (this.lastaction === 'rewind') {

				posTime -= this.elapsed;

			}

			// check if we have more songs to play on the current side..
			if (posTime >= this._getSide().current.getDuration()) {

				this._stop(buttons);
				this._setSidesPosStatus('end');
				return false;

			}

			this._resetElapsed();

			// given this, we need to know which song is playing at this point in time,
			// and from which point in time within the song we will play
			var data = this._getSongInfoByTime(posTime);

			// update cntTime
			this.cntTime = posTime;
			// update timeIterator
			this.timeIterator = data.iterator;

			return data;

		},
		_rewind: function () {

			var _self = this,
				action = 'rewind';

			if (this._getSide().current.getPositionStatus() === 'start') {

				return false;

			}

			this._updateButtons(action);

			$.when(this.sound.play('click')).done(function () {

				_self._updateStatus(true);
				_self.isMoving = true;

				_self._updateAction(action);
				_self.sound.play('rewind', true);
				_self._setWheelAnimation('0.5s', action);
				_self._timer();

			});

		},
		_forward: function () {

			var _self = this,
				action = 'forward';

			if (this._getSide().current.getPositionStatus() === 'end') {

				return false;

			}

			this._updateButtons(action);

			$.when(this.sound.play('click')).done(function () {

				_self._updateStatus(true);
				_self.isMoving = true;

				_self._updateAction(action);
				_self.sound.play('fforward', true);
				_self._setWheelAnimation('0.5s', action);
				_self._timer();

			});

		},
		_stop: function (buttons) {

			if (!buttons) {

				this._updateButtons('stop');
				this.sound.play('click');

			}
			this.isMoving = false;
			this._stopWheels();
			this.audio.pause();
			this._stopTimer();

		},
		_clear: function () {

			this.$audioEl.children('source').remove();

		},
		_setSidesPosStatus: function (position) {

			this._getSide().current.setPositionStatus(position);

			switch (position) {

				case 'middle':
					this._getSide().reverse.setPositionStatus(position);
					break;
				case 'start':
					this._getSide().reverse.setPositionStatus('end');
					break;
				case 'end':
					this._getSide().reverse.setPositionStatus('start');
					break;

			}

		},
		// given a point in time for the current side, returns the respective song of that side and the respective time within the song
		_getSongInfoByTime: function (time) {

			var data = {
					songIdx: 0,
					timeInSong: 0
				},
				side = this._getSide().current,
				playlist = side.getPlaylist(),
				len = side.getPlaylistCount(),
				cntTime = 0;

			for (var i = 0; i < len; ++i) {

				var song = playlist[i],
					duration = song.getDuration();

				cntTime += duration;

				if (cntTime > time) {

					data.songIdx = i;
					data.timeInSong = time - (cntTime - duration);
					data.iterator = cntTime - duration;

					return data;

				}

			}

			return data;

		},
		_getWheelValues: function (x) {

			var T = this._getSide().current.getDuration(),
				val = {
					left: (this.currentSide === 1) ? (-70 / T) * x + 70 : (70 / T) * x,
					right: (this.currentSide === 1) ? (70 / T) * x : (-70 / T) * x + 70
				};
			return val;

		},
		_getPosTime: function () {

			var wleft = this.$wheelLeft.data('wheel'),
				wright = this.$wheelRight.data('wheel');

			if (wleft === undefined) {

				wleft = 70;

			}
			if (wright === undefined) {

				wright = 0;

			}

			var T = this._getSide().current.getDuration(),
				posTime = this.currentSide === 2 ? (T * wleft) / 70 : (T * wright) / 70;

			return posTime;

		},
		_updateWheelValue: function (wheelVal) {

			this.$wheelLeft.data('wheel', wheelVal.left).css({
				'box-shadow': '0 0 0 ' + wheelVal.left + 'px black'
			});
			this.$wheelRight.data('wheel', wheelVal.right).css({
				'box-shadow': '0 0 0 ' + wheelVal.right + 'px black'
			});

		},
		_setWheelAnimation: function (speed, mode) {

			var _self = this,
				anim = '';

			switch (this.currentSide) {

				case 1:
					if (mode === 'play' || mode === 'forward') {
						anim = 'rotateLeft';
					} else if (mode === 'rewind') {
						anim = 'rotateRight';
					}
					break;
				case 2:
					if (mode === 'play' || mode === 'forward') {
						anim = 'rotateRight';
					} else if (mode === 'rewind') {
						anim = 'rotateLeft';
					}
					break;

			}

			var animStyle = {
				'-webkit-animation': anim + ' ' + speed + ' linear infinite forwards',
				'-moz-animation': anim + ' ' + speed + ' linear infinite forwards',
				'-o-animation': anim + ' ' + speed + ' linear infinite forwards',
				'-ms-animation': anim + ' ' + speed + ' linear infinite forwards',
				'animation': anim + ' ' + speed + ' linear infinite forwards'
			};

			setTimeout(function () {

				_self.$wheelLeft.css(animStyle);
				_self.$wheelRight.css(animStyle);

			}, 0);

		},
		_stopWheels: function () {

			var wheelStyle = {
				'-webkit-animation': 'none',
				'-moz-animation': 'none',
				'-o-animation': 'none',
				'-ms-animation': 'none',
				'animation': 'none'
			}

			this.$wheelLeft.css(wheelStyle);
			this.$wheelRight.css(wheelStyle);

		},
		// credits: http://www.sitepoint.com/creating-accurate-timers-in-javascript/
		_timer: function () {

			var _self = this,
				start = new Date().getTime(),
				time = 0;

			this._resetElapsed();

			this.isSeeking = true;

			this._setSidesPosStatus('middle');

			if (this.isSeeking) {

				clearTimeout(this.timertimeout);
				this.timertimeout = setTimeout(function () {

					_self._timerinstance(start, time);

				}, 100);

			}

		},
		_timerinstance: function (start, time) {

			var _self = this;

			time += 100;

			this.elapsed = Math.floor(time / 20) / 10;

			if (Math.round(this.elapsed) == this.elapsed) {

				this.elapsed += 0.0;

			}

			// stop if it reaches the end of the cassette / side
			// or if it reaches the beginning
			var posTime = this.cntTime;

			if (this.lastaction === 'forward') {

				posTime += this.elapsed;

			} else if (this.lastaction === 'rewind') {

				posTime -= this.elapsed;

			}

			var wheelVal = this._getWheelValues(posTime);
			this._updateWheelValue(wheelVal);

			if (posTime >= this._getSide().current.getDuration() || posTime <= 0) {

				this._stop();
				(posTime <= 0) ? this.cntTime = 0: this.cntTime = posTime;
				this._resetElapsed();
				(posTime <= 0) ? this._setSidesPosStatus('start'): this._setSidesPosStatus('end');
				return false;

			}

			var diff = (new Date().getTime() - start) - time;

			if (this.isSeeking) {

				clearTimeout(this.timertimeout);
				this.timertimeout = setTimeout(function () {

					_self._timerinstance(start, time);

				}, (100 - diff));

			}

		},
		_stopTimer: function () {

			clearTimeout(this.timertimeout);
			this.isSeeking = false;

		},
		_resetElapsed: function () {

			this.elapsed = 0.0;

		}

	};

	// Cassette side obj
	$.Side = function (id, playlist, status) {

		// side's name / id
		this.id = id;
		// status is "start", "middle" or "end"
		this.status = status;
		// array of songs sorted by song id
		this.playlist = playlist.sort(function (a, b) {

			var aid = a.id,
				bid = b.id;

			return ((aid < bid) ? -1 : ((aid > bid) ? 1 : 0));

		});
		// set playlist duration
		this._setDuration();
		// total number of songs
		this.playlistCount = playlist.length;

	};

	$.Side.prototype = {

		getSong: function (num) {

			return this.playlist[num];

		},
		getPlaylist: function () {

			return this.playlist;

		},
		_setDuration: function () {

			this.duration = 0;

			for (var i = 0, len = this.playlist.length; i < len; ++i) {

				this.duration += this.playlist[i].duration;

			}

		},
		getDuration: function () {

			return this.duration;

		},
		getPlaylistCount: function () {

			return this.playlistCount;

		},
		setPositionStatus: function (status) {

			this.status = status;

		},
		getPositionStatus: function () {

			return this.status;

		}

	};

	// Song obj
	$.Song = function (name, id) {

		this.id = id;
		this.name = name;
		this._init();

	};

	$.Song.prototype = {

		_init: function () {

			this.sources = {
				mp3: 'songs/' + this.name + '.mp3',
				ogg: 'songs/' + this.name + '.ogg'
			};

		},
		getSource: function (type) {

			return this.sources[type];

		},
		// load metadata to get the duration of the song
		loadMetadata: function () {

			var _self = this;

			return $.Deferred(

				function (dfd) {

					var $tmpAudio = $('<audio/>'),
						songsrc = _self.getSource(aux.getSupportedType());

					$tmpAudio.attr('preload', 'auto');
					$tmpAudio.attr('src', songsrc);

					$tmpAudio.on('loadedmetadata', function (event) {

						_self.duration = $tmpAudio.get(0).duration;
						dfd.resolve(_self);

					});

				}

			).promise();

		},
		getDuration: function () {

			return this.duration;

		}

	};

	// Sound obj
	$.Sound = function () {

		this._init();

	};

	$.Sound.prototype = {

		_init: function () {

			this.$audio = $('<audio/>').attr('preload', 'auto');

		},
		getSource: function (type) {

			return 'sounds/' + this.action + '.' + type;

		},
		play: function (action, loop) {

			var _self = this;
			return $.Deferred(function (dfd) {

				_self.action = action;

				var soundsrc = _self.getSource(aux.getSupportedType());

				_self.$audio.attr('src', soundsrc);
				if (loop) {

					_self.$audio.attr('loop', loop);

				} else {

					_self.$audio.removeAttr('loop');

				}

				_self.$audio.on('canplay', function (event) {

					// TODO: change timeout to ended event . ended event does not trigger for safari ?
					setTimeout(function () {

						dfd.resolve();

					}, 500);
					$(this).get(0).play();

				});

			});

		}

	};

	var logError = function (message) {

		if (window.console) {

			window.console.error(message);

		}

	};

	$.fn.cassette = function (options) {

		if (typeof options === 'string') {

			var args = Array.prototype.slice.call(arguments, 1);

			this.each(function () {

				var instance = $.data(this, 'cassette');

				if (!instance) {

					logError("cannot call methods on cassette prior to initialization; " +
						"attempted to call method '" + options + "'");
					return;

				}

				if (!$.isFunction(instance[options]) || options.charAt(0) === "_") {

					logError("no such method '" + options + "' for cassette instance");
					return;

				}

				instance[options].apply(instance, args);

			});

		} else {

			this.each(function () {

				var instance = $.data(this, 'cassette');
				if (!instance) {
					$.data(this, 'cassette', new $.Cassette(options, this));
				}
			});

		}

		return this;

	};

})(window, jQuery);